package cn.zwk.orchestrator;

import cn.zwk.config.ColumnBinding;
import cn.zwk.config.ColumnSelectorConfig;
import cn.zwk.config.HandlerDef;
import cn.zwk.config.ParseOptionsConfig;
import cn.zwk.config.PipelineConfig;
import cn.zwk.excel.ColumnSelector;
import cn.zwk.excel.ParseOptions;
import cn.zwk.excel.ParseResult;
import cn.zwk.excel.ParsedRow;
import cn.zwk.excel.QueryItem;
import cn.zwk.handler.Handler;
import cn.zwk.handler.HandlerFactory;
import cn.zwk.orchestrator.model.CellResult;
import cn.zwk.orchestrator.model.ReportPayload;
import cn.zwk.orchestrator.model.RowResult;
import cn.zwk.service.impl.EasyExcelSpreadsheetParser;
import cn.zwk.template.FreemarkerRenderer;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;

public class PipelineOrchestrator {

    public ReportPayload execute(PipelineConfig cfg) {
        Objects.requireNonNull(cfg, "cfg");

        // 1) Build ParseOptions from cfg
        ParseOptions options = buildParseOptions(cfg.getParse(), cfg.getColumns());

        // 2) Parse Excel
        EasyExcelSpreadsheetParser parser = new EasyExcelSpreadsheetParser();
        ParseResult result = parser.parse(Path.of(cfg.getExcelFile()), options);

        // 3) Prepare bindings and handlers per column (ordered)
        List<ColumnBinding> bindings = cfg.getColumns();
        Map<String, Handler> handlerCache = new HashMap<>();
        List<String> headers = new ArrayList<>();
        List<Integer> columnIndexes = new ArrayList<>();

        for (ColumnBinding b : bindings) {
            String header = b.getSelector() != null && b.getSelector().getHeaderName() != null
                    ? b.getSelector().getHeaderName()
                    : (b.getSelector() != null && b.getSelector().getIndex() != null
                        ? ("col-" + b.getSelector().getIndex()) : "col");
            headers.add(header);

            Integer idx = (b.getSelector() != null) ? b.getSelector().getIndex() : null;
            columnIndexes.add(idx); // may be null, matching by header below

            // warm handler cache
            handlerCache.computeIfAbsent(b.getHandler(), name -> {
                HandlerDef def = cfg.getHandlers().get(name);
                if (def == null) throw new IllegalArgumentException("Handler not found: " + name);
                return HandlerFactory.create(def.getType());
            });
        }

        // 4) Aggregate per-row execution
        List<RowResult> rows = new ArrayList<>();
        for (ParsedRow pr : result.rows()) {
            if (pr.isSkipped()) continue; // 跳过空key/合并等

            // Build "row" array for handler context: use queries of binding order (may be null)
            String[] rowQueries = new String[bindings.size()];
            for (int i = 0; i < bindings.size(); i++) {
                String q = findQueryForBinding(pr.queries(), bindings.get(i));
                rowQueries[i] = q;
            }

            List<CellResult> cellResults = new ArrayList<>();
            int succeed = 0;
            for (int i = 0; i < bindings.size(); i++) {
                ColumnBinding b = bindings.get(i);
                String handlerName = b.getHandler();
                HandlerDef def = cfg.getHandlers().get(handlerName);
                if (def == null) throw new IllegalArgumentException("Handler not found: " + handlerName);
                Handler handler = handlerCache.get(handlerName);

                // merge envs: cfg.envs <- def.envs <- b.envs
                Map<String, Object> envs = new LinkedHashMap<>();
                if (cfg.getEnvs() != null) envs.putAll(cfg.getEnvs());
                if (def.getEnvs() != null) envs.putAll(def.getEnvs());
                if (b.getEnvs() != null) envs.putAll(b.getEnvs());

                String ql = rowQueries[i];
                if (ql == null || ql.isBlank()) {
                    cellResults.add(new CellResult(null, 0, "单元格为空", null));
                    continue;
                }

                Object res = handler.beforeHandle(envs, pr.key(), ql, rowQueries);
                boolean ok = handler.handleStatus(envs, pr.key(), ql, rowQueries, res);
                String snippet = handler.handleSnippet(envs, pr.key(), ql, rowQueries, res);
                int status = ok ? 1 : 0;
                if (ok) succeed++;
                cellResults.add(new CellResult(ql, status, snippet, res));
            }

            rows.add(new RowResult(pr.key(), cellResults, succeed));
        }

        return new ReportPayload(cfg.getEnvs() == null ? Map.of() : cfg.getEnvs(), headers, rows);
    }

    public void writeReport(ReportPayload payload, Path templateFile, Path outputFile) {
        try {
            String tpl = Files.readString(templateFile, StandardCharsets.UTF_8);
            Map<String, Object> model = new HashMap<>();
            model.put("envs", payload.getEnvs());
            model.put("headers", payload.getHeaders());
            model.put("data", payload.getData());
            String out = FreemarkerRenderer.renderString(tpl, model);
            Files.createDirectories(outputFile.getParent());
            Files.writeString(outputFile, out, StandardCharsets.UTF_8);
        } catch (IOException e) {
            throw new RuntimeException("Failed to write report: " + outputFile, e);
        }
    }

    private static ParseOptions buildParseOptions(ParseOptionsConfig cfg, List<ColumnBinding> columns) {
        List<ColumnSelector> stmts = columns == null ? List.of() : columns.stream()
                .map(ColumnBinding::getSelector)
                .map(PipelineOrchestrator::toSelector)
                .collect(Collectors.toList());
        return ParseOptions.builder()
                .sheetName(cfg == null ? null : cfg.getSheetName())
                .headerRowIndex(cfg == null ? 0 : cfg.getHeaderRowIndex())
                .keyColumn(toSelector(cfg == null ? null : cfg.getKeyColumn()))
                .statementColumns(stmts)
                .skipMergedCells(cfg == null || cfg.isSkipMergedCells())
                .skipEmptyKeyRows(cfg == null || cfg.isSkipEmptyKeyRows())
                .build();
    }

    private static ColumnSelector toSelector(ColumnSelectorConfig c) {
        if (c == null) return ColumnSelector.byIndex(0);
        if (c.getIndex() != null) return ColumnSelector.byIndex(c.getIndex());
        if (c.getHeaderName() != null) return ColumnSelector.byHeader(c.getHeaderName());
        return ColumnSelector.byIndex(0);
    }

    private static String findQueryForBinding(List<QueryItem> items, ColumnBinding binding) {
        if (items == null || items.isEmpty()) return null;
        ColumnSelectorConfig sel = binding.getSelector();
        Optional<QueryItem> byIndex = Optional.empty();
        if (sel != null && sel.getIndex() != null) {
            byIndex = items.stream().filter(q -> q.column().index() == sel.getIndex()).findFirst();
        }
        if (byIndex.isPresent()) return byIndex.get().query();
        if (sel != null && sel.getHeaderName() != null) {
            Optional<QueryItem> byHeader = items.stream()
                    .filter(q -> sel.getHeaderName().equals(q.column().headerName()))
                    .findFirst();
            if (byHeader.isPresent()) return byHeader.get().query();
        }
        // fallback: if only one item and selector unspecified
        if (sel == null && items.size() == 1) return items.get(0).query();
        return null;
    }
}

